/*
 * Copyright (c) 2001-2006 Caucho Technology, Inc.  All rights reserved.
 *
 * The Apache Software License, Version 1.1
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Caucho Technology (http://www.caucho.com/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "Burlap", "Resin", and "Caucho" must not be used to
 *    endorse or promote products derived from this software without prior
 *    written permission. For written permission, please contact
 *    info@caucho.com.
 *
 * 5. Products derived from this software may not be called "Resin"
 *    nor may "Resin" appear in their names without prior written
 *    permission of Caucho Technology.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL CAUCHO TECHNOLOGY OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author Scott Ferguson
 */

package com.caucho.hessian.io;

import com.alipay.hessian.ClassNameResolver;
import com.caucho.hessian.util.IdentityIntMap;

import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;

/**
 * Output stream for Hessian 2 requests.
 *
 * <p>Since HessianOutput does not depend on any classes other than
 * in the JDK, it can be extracted independently into a smaller package.
 *
 * <p>HessianOutput is unbuffered, so any client needs to provide
 * its own buffering.
 *
 * <pre>
 * OutputStream os = ...; // from http connection
 * Hessian2Output out = new Hessian11Output(os);
 * String value;
 *
 * out.startCall("hello");  // start hello call
 * out.writeString("arg1"); // write a string argument
 * out.completeCall();      // complete the call
 * </pre>
 */
public class Hessian2Output
                           extends AbstractHessianOutput
                                                        implements Hessian2Constants {
    // the output stream/
    protected OutputStream   _os;

    // map of references
    private IdentityIntMap   _refs          = new IdentityIntMap();

    private HashMap          _serializerMap = new HashMap();

    private boolean          _isCloseStreamOnClose;

    // map of classes
    private HashMap          _classRefs;

    // map of types
    private HashMap          _typeRefs;

    private final static int SIZE           = 1024;

    private final byte[]     _buffer        = new byte[SIZE];
    private int              _offset;

    private boolean          _isStreaming;

    /**
     * Creates a new Hessian output stream, initialized with an
     * underlying output stream.
     *
     * @param os the underlying output stream.
     */
    public Hessian2Output(OutputStream os) {
        _os = os;
    }

    public void setCloseStreamOnClose(boolean isClose) {
        _isCloseStreamOnClose = isClose;
    }

    public boolean isCloseStreamOnClose() {
        return _isCloseStreamOnClose;
    }

    /**
     * Writes a complete method call.
     */
    public void call(String method, Object[] args)
        throws IOException {
        startCall(method);

        if (args != null) {
            for (int i = 0; i < args.length; i++)
                writeObject(args[i]);
        }

        completeCall();
    }

    /**
     * Starts the method call.  Clients would use <code>startCall</code>
     * instead of <code>call</code> if they wanted finer control over
     * writing the arguments, or needed to write headers.
     *
     * <code><pre>
     * c major minor
     * m b16 b8 method-name
     * </pre></code>
     *
     * @param method the method name to call.
     */
    public void startCall(String method)
        throws IOException {
        int offset = _offset;

        if (SIZE < offset + 32) {
            flush();
            offset = 0;
        }

        byte[] buffer = _buffer;

        buffer[offset++] = (byte) 'c';
        buffer[offset++] = (byte) 2;
        buffer[offset++] = (byte) 0;

        buffer[offset++] = (byte) 'm';
        int len = method.length();
        buffer[offset++] = (byte) (len >> 8);
        buffer[offset++] = (byte) len;

        _offset = offset;

        printString(method, 0, len);
    }

    /**
     * Writes the call tag.  This would be followed by the
     * headers and the method tag.
     *
     * <code><pre>
     * c major minor
     * </pre></code>
     */
    public void startCall()
        throws IOException {
        flushIfFull();

        int offset = _offset;
        byte[] buffer = _buffer;

        buffer[offset++] = (byte) 'c';
        buffer[offset++] = (byte) 2;
        buffer[offset++] = (byte) 0;
    }

    /**
     * Writes the streaming call tag.  This would be followed by the
     * headers and the method tag.
     *
     * <code><pre>
     * C major minor
     * </pre></code>
     */
    public void startStreamingCall()
        throws IOException {
        flushIfFull();

        int offset = _offset;
        byte[] buffer = _buffer;

        buffer[offset++] = (byte) 'C';
        buffer[offset++] = (byte) 2;
        buffer[offset++] = (byte) 0;
    }

    /**
     * Starts an envelope.
     *
     * <code><pre>
     * E major minor
     * m b16 b8 method-name
     * </pre></code>
     *
     * @param method the method name to call.
     */
    public void startEnvelope(String method)
        throws IOException {
        int offset = _offset;

        if (SIZE < offset + 32) {
            flush();
            offset = 0;
        }

        byte[] buffer = _buffer;

        buffer[offset++] = (byte) 'E';
        buffer[offset++] = (byte) 2;
        buffer[offset++] = (byte) 0;

        buffer[offset++] = (byte) 'm';
        int len = method.length();
        buffer[offset++] = (byte) (len >> 8);
        buffer[offset++] = (byte) len;

        _offset = offset;

        printString(method, 0, len);
    }

    /**
     * Completes an envelope.
     *
     * <p>A successful completion will have a single value:
     *
     * <pre>
     * z
     * </pre>
     */
    public void completeEnvelope()
        throws IOException {
        flushIfFull();

        _buffer[_offset++] = (byte) 'z';
    }

    /**
     * Writes the method tag.
     *
     * <code><pre>
     * m b16 b8 method-name
     * </pre></code>
     *
     * @param method the method name to call.
     */
    public void writeMethod(String method)
        throws IOException {
        flushIfFull();

        byte[] buffer = _buffer;
        int offset = _offset;

        buffer[offset++] = (byte) 'm';
        int len = method.length();
        buffer[offset++] = (byte) (len >> 8);
        buffer[offset++] = (byte) len;

        _offset = offset;

        printString(method, 0, len);
    }

    /**
     * Completes.
     *
     * <code><pre>
     * z
     * </pre></code>
     */
    public void completeCall()
        throws IOException {
        flushIfFull();

        _buffer[_offset++] = (byte) 'z';
    }

    /**
     * Starts the reply
     *
     * <p>A successful completion will have a single value:
     *
     * <pre>
     * r
     * </pre>
     */
    public void startReply()
        throws IOException {
        flushIfFull();

        _buffer[_offset++] = (byte) 'r';
        _buffer[_offset++] = (byte) 2;
        _buffer[_offset++] = (byte) 0;
    }

    /**
     * Starts the streaming reply
     *
     * <p>A successful completion will have a single value:
     *
     * <pre>
     * r
     * </pre>
     */
    public void startStreamingReply()
        throws IOException {
        flushIfFull();

        _buffer[_offset++] = (byte) 'R';
        _buffer[_offset++] = (byte) 2;
        _buffer[_offset++] = (byte) 0;
    }

    /**
     * Completes reading the reply
     *
     * <p>A successful completion will have a single value:
     *
     * <pre>
     * z
     * </pre>
     */
    public void completeReply()
        throws IOException {
        flushIfFull();

        _buffer[_offset++] = (byte) 'z';
    }

    /**
     * Starts the message
     *
     * <p>A message contains several objects followed by a 'z'</p>
     *
     * <pre>
     * p x02 x00
     * </pre>
     */
    public void startMessage()
        throws IOException {
        flushIfFull();

        _buffer[_offset++] = (byte) 'p';
        _buffer[_offset++] = (byte) 2;
        _buffer[_offset++] = (byte) 0;
    }

    /**
     * Completes reading the message
     *
     * <p>A successful completion will have a single value:
     *
     * <pre>
     * z
     * </pre>
     */
    public void completeMessage()
        throws IOException {
        flushIfFull();

        _buffer[_offset++] = (byte) 'z';
    }

    /**
     * Writes a header name.  The header value must immediately follow.
     *
     * <code><pre>
     * H b16 b8 foo <em>value</em>
     * </pre></code>
     */
    public void writeHeader(String name)
        throws IOException {
        int len = name.length();

        flushIfFull();

        _buffer[_offset++] = (byte) 'H';
        _buffer[_offset++] = (byte) (len >> 8);
        _buffer[_offset++] = (byte) (len);

        printString(name);
    }

    /**
     * Writes a fault.  The fault will be written
     * as a descriptive string followed by an object:
     *
     * <code><pre>
     * f
     * &lt;string>code
     * &lt;string>the fault code
     *
     * &lt;string>message
     * &lt;string>the fault mesage
     *
     * &lt;string>detail
     * mt\x00\xnnjavax.ejb.FinderException
     *     ...
     * z
     * z
     * </pre></code>
     *
     * @param code the fault code, a three digit
     */
    public void writeFault(String code, String message, Object detail)
        throws IOException {
        flushIfFull();

        _buffer[_offset++] = (byte) 'f';
        writeString("code");
        writeString(code);

        writeString("message");
        writeString(message);

        if (detail != null) {
            writeString("detail");
            writeObject(detail);
        }

        flushIfFull();
        _buffer[_offset++] = (byte) ('z');
    }

    /**
     * Writes any object to the output stream.
     */
    public void writeObject(Object object)
        throws IOException {
        if (object == null) {
            writeNull();
            return;
        }

        Serializer serializer;

        // add by zhanggeng @2017-7-23
        Class clazz = object.getClass();
        ClassNameResolver resolver = findSerializerFactory().getClassNameResolver();
        if (resolver != null) {
            resolver.resolve(clazz.getName());
        }

        serializer = findSerializerFactory().getSerializer(clazz);

        serializer.writeObject(object, this);
    }

    /**
     * Writes the list header to the stream.  List writers will call
     * <code>writeListBegin</code> followed by the list contents and then
     * call <code>writeListEnd</code>.
     *
     * <code><pre>
     * V
     * t b16 b8 type
     * l b32 b24 b16 b8
     * </pre></code>
     */
    public boolean writeListBegin(int length, String type)
        throws IOException {
        flushIfFull();

        if (_typeRefs != null) {
            Integer refV = (Integer) _typeRefs.get(type);

            if (refV != null) {
                _buffer[_offset++] = (byte) (LIST_FIXED);
                writeInt(refV.intValue());
                writeInt(length);

                return false;
            }
        }

        _buffer[_offset++] = (byte) 'V';

        writeType(type);

        flushIfFull();

        if (length < 0) {
        } else if (length < 0x100) {
            _buffer[_offset++] = (byte) (LENGTH_BYTE);
            _buffer[_offset++] = (byte) (length);
        } else {
            _buffer[_offset++] = (byte) ('l');
            _buffer[_offset++] = (byte) (length >> 24);
            _buffer[_offset++] = (byte) (length >> 16);
            _buffer[_offset++] = (byte) (length >> 8);
            _buffer[_offset++] = (byte) (length);
        }

        return true;
    }

    /**
     * Writes the tail of the list to the stream.
     */
    public void writeListEnd()
        throws IOException {
        flushIfFull();

        _buffer[_offset++] = (byte) 'z';
    }

    /**
     * Writes the map header to the stream.  Map writers will call
     * <code>writeMapBegin</code> followed by the map contents and then
     * call <code>writeMapEnd</code>.
     *
     * <code><pre>
     * Mt b16 b8 (<key> <value>)z
     * </pre></code>
     */
    public void writeMapBegin(String type)
        throws IOException {
        if (SIZE < _offset + 32)
            flush();

        _buffer[_offset++] = 'M';

        writeType(type);
    }

    /**
     * Writes the tail of the map to the stream.
     */
    public void writeMapEnd()
        throws IOException {
        if (SIZE < _offset + 32)
            flush();

        _buffer[_offset++] = (byte) 'z';
    }

    /**
     * Writes the object definition
     *
     * <code><pre>
     * O t b16 b8 <string>*
     * </pre></code>
     */
    public int writeObjectBegin(String type)
        throws IOException {
        if (_classRefs == null)
            _classRefs = new HashMap();

        Integer refV = (Integer) _classRefs.get(type);

        if (refV != null) {
            int ref = refV.intValue();

            if (SIZE < _offset + 32)
                flush();

            _buffer[_offset++] = (byte) 'o';
            writeInt(ref);

            return ref;
        } else {
            int ref = _classRefs.size();

            _classRefs.put(type, new Integer(ref));

            if (SIZE < _offset + 32)
                flush();

            _buffer[_offset++] = (byte) 'O';

            int len = type.length();

            writeInt(len);

            printString(type, 0, len);

            return -1;
        }
    }

    /**
     * Writes the tail of the class definition to the stream.
     */
    public void writeClassFieldLength(int len)
        throws IOException {
        writeInt(len);
    }

    /**
     * Writes the tail of the object definition to the stream.
     */
    public void writeObjectEnd()
        throws IOException {
    }

    /**
     * Writes a remote object reference to the stream.  The type is the
     * type of the remote interface.
     *
     * <code><pre>
     * 'r' 't' b16 b8 type url
     * </pre></code>
     */
    public void writeRemote(String type, String url)
        throws IOException {
        if (SIZE < _offset + 32)
            flush();

        _buffer[_offset++] = (byte) 'r';

        writeType(type);

        if (SIZE < _offset + 32)
            flush();

        _buffer[_offset++] = (byte) 'S';

        printLenString(url);
    }

    private void writeType(String type)
        throws IOException {
        if (type == null)
            return;

        int len = type.length();
        if (len == 0)
            return;

        if (_typeRefs == null)
            _typeRefs = new HashMap();

        Integer typeRefV = (Integer) _typeRefs.get(type);

        if (typeRefV != null) {
            int typeRef = typeRefV.intValue();

            flushIfFull();

            _buffer[_offset++] = (byte) TYPE_REF;

            writeInt(typeRef);
        } else {
            _typeRefs.put(type, new Integer(_typeRefs.size()));

            if (SIZE < _offset + 32)
                flush();

            _buffer[_offset++] = (byte) 't';

            printLenString(type);
        }
    }

    /**
     * Writes a boolean value to the stream.  The boolean will be written
     * with the following syntax:
     *
     * <code><pre>
     * T
     * F
     * </pre></code>
     *
     * @param value the boolean value to write.
     */
    public void writeBoolean(boolean value)
        throws IOException {
        if (SIZE < _offset + 16)
            flush();

        if (value)
            _buffer[_offset++] = (byte) 'T';
        else
            _buffer[_offset++] = (byte) 'F';
    }

    /**
     * Writes an integer value to the stream.  The integer will be written
     * with the following syntax:
     *
     * <code><pre>
     * I b32 b24 b16 b8
     * </pre></code>
     *
     * @param value the integer value to write.
     */
    public void writeInt(int value)
        throws IOException {
        int offset = _offset;
        byte[] buffer = _buffer;

        if (SIZE <= offset + 16) {
            flush();
            offset = 0;
        }

        if (INT_DIRECT_MIN <= value && value <= INT_DIRECT_MAX)
            buffer[offset++] = (byte) (value + INT_ZERO);
        else if (INT_BYTE_MIN <= value && value <= INT_BYTE_MAX) {
            buffer[offset++] = (byte) (INT_BYTE_ZERO + (value >> 8));
            buffer[offset++] = (byte) (value);
        } else if (INT_SHORT_MIN <= value && value <= INT_SHORT_MAX) {
            buffer[offset++] = (byte) (INT_SHORT_ZERO + (value >> 16));
            buffer[offset++] = (byte) (value >> 8);
            buffer[offset++] = (byte) (value);
        } else {
            buffer[offset++] = (byte) ('I');
            buffer[offset++] = (byte) (value >> 24);
            buffer[offset++] = (byte) (value >> 16);
            buffer[offset++] = (byte) (value >> 8);
            buffer[offset++] = (byte) (value);
        }

        _offset = offset;
    }

    /**
     * Writes a long value to the stream.  The long will be written
     * with the following syntax:
     *
     * <code><pre>
     * L b64 b56 b48 b40 b32 b24 b16 b8
     * </pre></code>
     *
     * @param value the long value to write.
     */
    public void writeLong(long value)
        throws IOException {
        int offset = _offset;
        byte[] buffer = _buffer;

        if (SIZE <= offset + 16) {
            flush();
            offset = 0;
        }

        if (LONG_DIRECT_MIN <= value && value <= LONG_DIRECT_MAX) {
            buffer[offset++] = (byte) (value + LONG_ZERO);
        } else if (LONG_BYTE_MIN <= value && value <= LONG_BYTE_MAX) {
            buffer[offset++] = (byte) (LONG_BYTE_ZERO + (value >> 8));
            buffer[offset++] = (byte) (value);
        } else if (LONG_SHORT_MIN <= value && value <= LONG_SHORT_MAX) {
            buffer[offset++] = (byte) (LONG_SHORT_ZERO + (value >> 16));
            buffer[offset++] = (byte) (value >> 8);
            buffer[offset++] = (byte) (value);
        } else if (-0x80000000L <= value && value <= 0x7fffffffL) {
            buffer[offset + 0] = (byte) LONG_INT;
            buffer[offset + 1] = (byte) (value >> 24);
            buffer[offset + 2] = (byte) (value >> 16);
            buffer[offset + 3] = (byte) (value >> 8);
            buffer[offset + 4] = (byte) (value);

            offset += 5;
        } else {
            buffer[offset + 0] = (byte) 'L';
            buffer[offset + 1] = (byte) (value >> 56);
            buffer[offset + 2] = (byte) (value >> 48);
            buffer[offset + 3] = (byte) (value >> 40);
            buffer[offset + 4] = (byte) (value >> 32);
            buffer[offset + 5] = (byte) (value >> 24);
            buffer[offset + 6] = (byte) (value >> 16);
            buffer[offset + 7] = (byte) (value >> 8);
            buffer[offset + 8] = (byte) (value);

            offset += 9;
        }

        _offset = offset;
    }

    /**
     * Writes a double value to the stream.  The double will be written
     * with the following syntax:
     *
     * <code><pre>
     * D b64 b56 b48 b40 b32 b24 b16 b8
     * </pre></code>
     *
     * @param value the double value to write.
     */
    public void writeDouble(double value)
        throws IOException {
        int offset = _offset;
        byte[] buffer = _buffer;

        if (SIZE <= offset + 16) {
            flush();
            offset = 0;
        }

        int intValue = (int) value;

        if (intValue == value) {
            if (intValue == 0) {
                buffer[offset++] = (byte) DOUBLE_ZERO;

                _offset = offset;

                return;
            } else if (intValue == 1) {
                buffer[offset++] = (byte) DOUBLE_ONE;

                _offset = offset;

                return;
            } else if (-0x80 <= intValue && intValue < 0x80) {
                buffer[offset++] = (byte) DOUBLE_BYTE;
                buffer[offset++] = (byte) intValue;

                _offset = offset;

                return;
            } else if (-0x8000 <= intValue && intValue < 0x8000) {
                buffer[offset + 0] = (byte) DOUBLE_SHORT;
                buffer[offset + 1] = (byte) (intValue >> 8);
                buffer[offset + 2] = (byte) intValue;

                _offset = offset + 3;

                return;
            }
        }

        float f = (float) value;

        if (f == value) {
            int bits = Float.floatToIntBits(f);

            buffer[offset + 0] = (byte) (DOUBLE_FLOAT);
            buffer[offset + 1] = (byte) (bits >> 24);
            buffer[offset + 2] = (byte) (bits >> 16);
            buffer[offset + 3] = (byte) (bits >> 8);
            buffer[offset + 4] = (byte) (bits);

            _offset = offset + 5;

            return;
        }

        long bits = Double.doubleToLongBits(value);

        buffer[offset + 0] = (byte) 'D';
        buffer[offset + 1] = (byte) (bits >> 56);
        buffer[offset + 2] = (byte) (bits >> 48);
        buffer[offset + 3] = (byte) (bits >> 40);
        buffer[offset + 4] = (byte) (bits >> 32);
        buffer[offset + 5] = (byte) (bits >> 24);
        buffer[offset + 6] = (byte) (bits >> 16);
        buffer[offset + 7] = (byte) (bits >> 8);
        buffer[offset + 8] = (byte) (bits);

        _offset = offset + 9;
    }

    /**
     * Writes a date to the stream.
     *
     * <code><pre>
     * T  b64 b56 b48 b40 b32 b24 b16 b8
     * </pre></code>
     *
     * @param time the date in milliseconds from the epoch in UTC
     */
    public void writeUTCDate(long time)
        throws IOException {
        if (SIZE < _offset + 32)
            flush();

        int offset = _offset;
        byte[] buffer = _buffer;

        buffer[offset++] = (byte) ('d');
        buffer[offset++] = ((byte) (time >> 56));
        buffer[offset++] = ((byte) (time >> 48));
        buffer[offset++] = ((byte) (time >> 40));
        buffer[offset++] = ((byte) (time >> 32));
        buffer[offset++] = ((byte) (time >> 24));
        buffer[offset++] = ((byte) (time >> 16));
        buffer[offset++] = ((byte) (time >> 8));
        buffer[offset++] = ((byte) (time));

        _offset = offset;
    }

    /**
     * Writes a null value to the stream.
     * The null will be written with the following syntax
     *
     * <code><pre>
     * N
     * </pre></code>
     */
    public void writeNull()
        throws IOException {
        int offset = _offset;
        byte[] buffer = _buffer;

        if (SIZE <= offset + 16) {
            flush();
            offset = 0;
        }

        buffer[offset++] = 'N';

        _offset = offset;
    }

    /**
     * Writes a string value to the stream using UTF-8 encoding.
     * The string will be written with the following syntax:
     *
     * <code><pre>
     * S b16 b8 string-value
     * </pre></code>
     * <p>
     * If the value is null, it will be written as
     *
     * <code><pre>
     * N
     * </pre></code>
     *
     * @param value the string value to write.
     */
    public void writeString(String value)
        throws IOException {
        int offset = _offset;
        byte[] buffer = _buffer;

        if (SIZE <= offset + 16) {
            flush();
            offset = 0;
        }

        if (value == null) {
            buffer[offset++] = (byte) 'N';

            _offset = offset;
        } else {
            int length = value.length();
            int strOffset = 0;

            while (length > 0x8000) {
                int sublen = 0x8000;

                offset = _offset;

                if (SIZE <= offset + 16) {
                    flush();
                    offset = 0;
                }

                // chunk can't end in high surrogate
                char tail = value.charAt(strOffset + sublen - 1);

                if (0xd800 <= tail && tail <= 0xdbff)
                    sublen--;

                buffer[offset + 0] = (byte) 's';
                buffer[offset + 1] = (byte) (sublen >> 8);
                buffer[offset + 2] = (byte) (sublen);

                _offset = offset + 3;

                printString(value, strOffset, sublen);

                length -= sublen;
                strOffset += sublen;
            }

            offset = _offset;

            if (SIZE <= offset + 16) {
                flush();
                offset = 0;
            }

            if (length <= STRING_DIRECT_MAX) {
                buffer[offset++] = (byte) (STRING_DIRECT + length);
            } else {
                buffer[offset++] = (byte) ('S');
                buffer[offset++] = (byte) (length >> 8);
                buffer[offset++] = (byte) (length);
            }

            _offset = offset;

            printString(value, strOffset, length);
        }
    }

    /**
     * Writes a string value to the stream using UTF-8 encoding.
     * The string will be written with the following syntax:
     *
     * <code><pre>
     * S b16 b8 string-value
     * </pre></code>
     * <p>
     * If the value is null, it will be written as
     *
     * <code><pre>
     * N
     * </pre></code>
     *
     * @param buffer
     * @param offset
     * @param length
     */
    public void writeString(char[] buffer, int offset, int length)
        throws IOException {
        if (buffer == null) {
            if (SIZE < _offset + 16)
                flush();

            _buffer[_offset++] = (byte) ('N');
        } else {
            while (length > 0x8000) {
                int sublen = 0x8000;

                if (SIZE < _offset + 16)
                    flush();

                // chunk can't end in high surrogate
                char tail = buffer[offset + sublen - 1];

                if (0xd800 <= tail && tail <= 0xdbff)
                    sublen--;

                _buffer[_offset++] = (byte) 's';
                _buffer[_offset++] = (byte) (sublen >> 8);
                _buffer[_offset++] = (byte) (sublen);

                printString(buffer, offset, sublen);

                length -= sublen;
                offset += sublen;
            }

            if (SIZE < _offset + 16)
                flush();

            if (length <= STRING_DIRECT_MAX) {
                _buffer[_offset++] = (byte) (STRING_DIRECT + length);
            } else {
                _buffer[_offset++] = (byte) ('S');
                _buffer[_offset++] = (byte) (length >> 8);
                _buffer[_offset++] = (byte) (length);
            }

            printString(buffer, offset, length);
        }
    }

    /**
     * Writes a byte array to the stream.
     * The array will be written with the following syntax:
     *
     * <code><pre>
     * B b16 b18 bytes
     * </pre></code>
     * <p>
     * If the value is null, it will be written as
     *
     * <code><pre>
     * N
     * </pre></code>
     *
     * @param buffer
     */
    public void writeBytes(byte[] buffer)
        throws IOException {
        if (buffer == null) {
            if (SIZE < _offset + 16)
                flush();

            _buffer[_offset++] = 'N';
        } else
            writeBytes(buffer, 0, buffer.length);
    }

    /**
     * Writes a byte array to the stream.
     * The array will be written with the following syntax:
     *
     * <code><pre>
     * B b16 b18 bytes
     * </pre></code>
     * <p>
     * If the value is null, it will be written as
     *
     * <code><pre>
     * N
     * </pre></code>
     *
     * @param buffer
     * @param offset
     * @param length
     */
    public void writeBytes(byte[] buffer, int offset, int length)
        throws IOException {
        if (buffer == null) {
            if (SIZE < _offset + 16)
                flush();

            _buffer[_offset++] = (byte) 'N';
        } else {
            flush();

            while (length > 0x8000) {
                int sublen = 0x8000;

                _os.write('b');
                _os.write(sublen >> 8);
                _os.write(sublen);

                _os.write(buffer, offset, sublen);

                length -= sublen;
                offset += sublen;
            }

            if (length < 0x10) {
                _os.write(BYTES_DIRECT + length);
            } else {
                _os.write('B');
                _os.write(length >> 8);
                _os.write(length);
            }

            _os.write(buffer, offset, length);
        }
    }

    /**
     * Writes a byte buffer to the stream.
     *
     * <code><pre>
     * </pre></code>
     */
    public void writeByteBufferStart()
        throws IOException {
    }

    /**
     * Writes a byte buffer to the stream.
     *
     * <code><pre>
     * b b16 b18 bytes
     * </pre></code>
     */
    public void writeByteBufferPart(byte[] buffer, int offset, int length)
        throws IOException {
        while (length > 0) {
            int sublen = length;

            if (0x8000 < sublen)
                sublen = 0x8000;

            flush(); // bypass buffer

            _os.write('b');
            _os.write(sublen >> 8);
            _os.write(sublen);

            _os.write(buffer, offset, sublen);

            length -= sublen;
            offset += sublen;
        }
    }

    /**
     * Writes a byte buffer to the stream.
     *
     * <code><pre>
     * b b16 b18 bytes
     * </pre></code>
     */
    public void writeByteBufferEnd(byte[] buffer, int offset, int length)
        throws IOException {
        writeBytes(buffer, offset, length);
    }

    /**
     * Returns an output stream to write binary data.
     */
    public OutputStream getBytesOutputStream()
        throws IOException {
        return new BytesOutputStream();
    }

    /**
     * Writes a reference.
     *
     * <code><pre>
     * R b32 b24 b16 b8
     * </pre></code>
     *
     * @param value the integer value to write.
     */
    public void writeRef(int value)
        throws IOException {
        if (SIZE < _offset + 16)
            flush();

        if (value < 0x100) {
            _buffer[_offset++] = (byte) (REF_BYTE);
            _buffer[_offset++] = (byte) (value);
        } else if (value < 0x10000) {
            _buffer[_offset++] = (byte) (REF_SHORT);
            _buffer[_offset++] = (byte) (value >> 8);
            _buffer[_offset++] = (byte) (value);
        } else {
            _buffer[_offset++] = (byte) ('R');
            _buffer[_offset++] = (byte) (value >> 24);
            _buffer[_offset++] = (byte) (value >> 16);
            _buffer[_offset++] = (byte) (value >> 8);
            _buffer[_offset++] = (byte) (value);
        }
    }

    /**
     * If the object has already been written, just write its ref.
     *
     * @return true if we're writing a ref.
     */
    public boolean addRef(Object object)
        throws IOException {
        int ref = _refs.get(object);

        if (ref >= 0) {
            writeRef(ref);

            return true;
        } else {
            _refs.put(object, _refs.size());

            return false;
        }
    }

    /**
     * Removes a reference.
     */
    public boolean removeRef(Object obj)
        throws IOException {
        if (_refs != null) {
            _refs.remove(obj);

            return true;
        } else
            return false;
    }

    /**
     * Replaces a reference from one object to another.
     */
    public boolean replaceRef(Object oldRef, Object newRef)
        throws IOException {
        Integer value = (Integer) _refs.remove(oldRef);

        if (value != null) {
            _refs.put(newRef, value);
            return true;
        } else
            return false;
    }

    /**
     * Resets the references for streaming.
     */
    public void resetReferences() {
        if (_refs != null)
            _refs.clear();
    }

    /**
     * Starts the streaming message
     *
     * <p>A streaming message starts with 'P'</p>
     *
     * <pre>
     * P x02 x00
     * </pre>
     */
    public void writeStreamingObject(Object obj)
        throws IOException {
        if (_refs != null)
            _refs.clear();

        flush();

        _isStreaming = true;
        _offset = 3;

        writeObject(obj);

        int len = _offset - 3;

        _buffer[0] = (byte) 'P';
        _buffer[1] = (byte) (len >> 8);
        _buffer[2] = (byte) len;

        _isStreaming = false;

        flush();
    }

    /**
     * Prints a string to the stream, encoded as UTF-8 with preceeding length
     *
     * @param v the string to print.
     */
    public void printLenString(String v)
        throws IOException {
        if (SIZE < _offset + 16)
            flush();

        if (v == null) {
            _buffer[_offset++] = (byte) (0);
            _buffer[_offset++] = (byte) (0);
        } else {
            int len = v.length();
            _buffer[_offset++] = (byte) (len >> 8);
            _buffer[_offset++] = (byte) (len);

            printString(v, 0, len);
        }
    }

    /**
     * Prints a string to the stream, encoded as UTF-8
     *
     * @param v the string to print.
     */
    public void printString(String v)
        throws IOException {
        printString(v, 0, v.length());
    }

    /**
     * Prints a string to the stream, encoded as UTF-8
     *
     * @param v the string to print.
     */
    public void printString(String v, int strOffset, int length)
        throws IOException {
        int offset = _offset;
        byte[] buffer = _buffer;

        for (int i = 0; i < length; i++) {
            if (SIZE <= offset + 16) {
                _offset = offset;
                flush();
                offset = 0;
            }

            char ch = v.charAt(i + strOffset);

            if (ch < 0x80)
                buffer[offset++] = (byte) (ch);
            else if (ch < 0x800) {
                buffer[offset++] = (byte) (0xc0 + ((ch >> 6) & 0x1f));
                buffer[offset++] = (byte) (0x80 + (ch & 0x3f));
            } else {
                buffer[offset++] = (byte) (0xe0 + ((ch >> 12) & 0xf));
                buffer[offset++] = (byte) (0x80 + ((ch >> 6) & 0x3f));
                buffer[offset++] = (byte) (0x80 + (ch & 0x3f));
            }
        }

        _offset = offset;
    }

    /**
     * Prints a string to the stream, encoded as UTF-8
     *
     * @param v the string to print.
     */
    public void printString(char[] v, int strOffset, int length)
        throws IOException {
        int offset = _offset;
        byte[] buffer = _buffer;

        for (int i = 0; i < length; i++) {
            if (SIZE <= offset + 16) {
                _offset = offset;
                flush();
                offset = 0;
            }

            char ch = v[i + strOffset];

            if (ch < 0x80)
                buffer[offset++] = (byte) (ch);
            else if (ch < 0x800) {
                buffer[offset++] = (byte) (0xc0 + ((ch >> 6) & 0x1f));
                buffer[offset++] = (byte) (0x80 + (ch & 0x3f));
            } else {
                buffer[offset++] = (byte) (0xe0 + ((ch >> 12) & 0xf));
                buffer[offset++] = (byte) (0x80 + ((ch >> 6) & 0x3f));
                buffer[offset++] = (byte) (0x80 + (ch & 0x3f));
            }
        }

        _offset = offset;
    }

    private final void flushIfFull()
        throws IOException {
        int offset = _offset;

        if (SIZE < offset + 32) {
            _offset = 0;
            _os.write(_buffer, 0, offset);
        }
    }

    public final void flush()
        throws IOException {
        int offset = _offset;

        if (offset > 0) {
            if (_isStreaming) {
                int len = offset - 3;
                _buffer[0] = 'p';
                _buffer[1] = (byte) (len >> 8);
                _buffer[2] = (byte) len;
            }

            _offset = 0;
            _os.write(_buffer, 0, offset);
        }
    }

    public final void close()
        throws IOException {
        flush();

        OutputStream os = _os;
        _os = null;

        if (os != null) {
            if (_isCloseStreamOnClose)
                os.close();
        }
    }

    class BytesOutputStream extends OutputStream {
        private int _startOffset;

        BytesOutputStream()
                           throws IOException {
            if (SIZE < _offset + 16)
                flush();

            _startOffset = _offset;
            _offset += 3; // skip 'b' xNN xNN
        }

        public void write(int ch)
            throws IOException {
            if (SIZE <= _offset) {
                int length = (_offset - _startOffset) - 3;

                _buffer[_startOffset] = (byte) 'b';
                _buffer[_startOffset + 1] = (byte) (length >> 8);
                _buffer[_startOffset + 2] = (byte) (length);

                flush();

                _startOffset = _offset;
                _offset += 3;
            }

            _buffer[_offset++] = (byte) ch;
        }

        public void write(byte[] buffer, int offset, int length)
            throws IOException {
            while (length > 0) {
                int sublen = SIZE - _offset;

                if (length < sublen)
                    sublen = length;

                if (sublen > 0) {
                    System.arraycopy(buffer, offset, _buffer, _offset, sublen);
                    _offset += sublen;
                }

                length -= sublen;
                offset += sublen;

                if (SIZE <= _offset) {
                    int chunkLength = (_offset - _startOffset) - 3;

                    _buffer[_startOffset] = (byte) 'b';
                    _buffer[_startOffset + 1] = (byte) (chunkLength >> 8);
                    _buffer[_startOffset + 2] = (byte) (chunkLength);

                    flush();

                    _startOffset = _offset;
                    _offset += 3;
                }
            }
        }

        public void close()
            throws IOException {
            int startOffset = _startOffset;
            _startOffset = -1;

            if (startOffset < 0)
                return;

            int length = (_offset - startOffset) - 3;

            _buffer[startOffset] = (byte) 'B';
            _buffer[startOffset + 1] = (byte) (length >> 8);
            _buffer[startOffset + 2] = (byte) (length);
        }
    }
}
